using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Pinwheel.Toolkits.Generator
{
    partial class Generator
    {
        static void Generate(TypeDeclarationSyntax syntax, Compilation compilation, string? serializationInfoLogDirectoryPath, IGeneratorContext context)
        {
            var semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree);

            var typeSymbol = semanticModel.GetDeclaredSymbol(syntax, context.CancellationToken);
            if (typeSymbol == null)
            {
                return;
            }
            
            // verify is partial
            if (!IsPartial(syntax))
            {
                context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MustBePartial, syntax.Identifier.GetLocation(), typeSymbol.Name));
                return;
            }
            
            var reference = new ReferenceSymbols(compilation);
            var typeMeta = new TypeMeta(typeSymbol, reference);
            
            // ReportDiagnostic when validate failed.
            if (!typeMeta.Validate(syntax, context))
            {
                return;
            } 
            
            var fullType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
                .Replace("global::", "")
                .Replace("<", "_")
                .Replace(">", "_");
            
            var sb = new StringBuilder();
            sb.AppendLine($@"
// <auto-generated/>

using System;
using {Definition.LSerializeNameSpace};
using {Definition.LSerializeNameSpace}.Formatters;
");
            var ns = typeMeta.Symbol.ContainingNamespace;
            
            if (!ns.IsGlobalNamespace) sb.AppendLine($"namespace {ns} \n{{");
            typeMeta.Emit(sb, context);
            if (!ns.IsGlobalNamespace) sb.AppendLine($"}}");
            
            var code = sb.ToString();
            context.AddSource($"{fullType}.LSerialize.Gen.cs", code);
        }
        
        static bool IsPartial(TypeDeclarationSyntax typeDeclaration)
        {
            return typeDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
        }
        
        static bool IsNested(TypeDeclarationSyntax typeDeclaration)
        {
            return typeDeclaration.Parent is TypeDeclarationSyntax;
        }
    }

    public partial class TypeMeta
    {
        public void Emit(StringBuilder writer, IGeneratorContext context)
        {
            
            EmitSerializeImplement(writer, context);
            EmitExtensionImplement(writer, context);
            EmitFormatterImplement(writer, context);
        }

        void EmitSerializeImplement(StringBuilder writer, IGeneratorContext context)
        {
            var typeKind = IsValueType ? "struct" : "class"; 
            writer.AppendLine($@"
    public unsafe partial {typeKind} {TypeName} : {Definition.LSerializeBaseType}<{TypeName}>
    {{
        {EmitSerializeImplement()}
        {EmitGetValidFieldCount()}
    }}"); 
        }
        
        void EmitExtensionImplement(StringBuilder writer, IGeneratorContext context)
        {
            var typeKind = IsValueType ? "struct" : "class"; 
            writer.AppendLine($@"
    public unsafe partial {typeKind} {TypeName} : {Definition.LSerializeIEquatable}<{TypeName}>
    {{
        const int BaseHash = {FullName.GetHashCode()};
        {EmitEquatableCode()}
        {EmitGetHashCode()}
    }}");
        }

        string EmitSerializeImplement()
        {
            var serializeBody = Members.Where(x => x.Symbol != null)
                .Select(x => x.EmitSerialize(Definition.Indents12Spaces)).NewLine();
            var deserializeBody = Members.Where(x => x.Symbol != null)
                .Select(x => x.EmitDeserialize(Definition.Indents12Spaces)).NewLine();
            
            return $@"
        [Preserve]
        public static void Serialize({Definition.LSerializeContext}* context, {Definition.LStreamWriter}* writer, ref {TypeName} value)
        {{
            context->SetValidFieldCount(GetValidFieldCount(ref value));
{serializeBody}           
        }}

        [Preserve]
        public static void Deserialize({Definition.LSerializeContext}* context, {Definition.LStreamReader}* reader, ref {TypeName} value)
        {{
            value = new {TypeName}();
{deserializeBody}    
        }}";
        }
        
        string EmitGetValidFieldCount()
        {
            string body = Members.Where(x => x.Symbol != null)
                .Select(x => x.EmitValidFieldCount(Definition.Indents12Spaces)).NewLine();
            return $@"
        private static int GetValidFieldCount(ref {TypeName} value)
        {{
            int total = 0;
{body}
            return total;
        }}";
        }

        string EmitEquatableCode()
        {
            string checkRef = IsValueType ? "" : $"\n{Definition.Indents12Spaces}if (null == value) return false;";
            string checkRefStatic = IsValueType ? "" : @"
            if (ReferenceEquals(v1, v2))
            {
                return true;
            }

            if (v1 is null || v2 is null)
            {
                return false;
            }";
            string operatorBody = Members.Length > 0
                ? Members.Where(x => x.Symbol != null)
                    .Select(x => x.EmitEquatableCode(Definition.Indents16Spaces, Members.Length, true)).NewLine()
                : "true";
            return $@"
        #region Operator
        public static bool operator ==({TypeName} v1, {TypeName} v2)
        {{{checkRefStatic}
            return {operatorBody};
        }}
        
        public static bool operator !=({TypeName} v1, {TypeName} v2)
        {{
            return !(v1 == v2);
        }}
        #endregion

        public bool Equals({TypeName} value)
        {{{checkRef}
            return this == value;
        }}

        public override bool Equals(object obj)
        {{
            if (obj is {TypeName} value)
            {{
                return this == value;
            }}
            return false;
        }}";
        }
        
        string EmitGetHashCode()
        {
            string body = Members.Where(x => x.Symbol != null)
                .Select(x => x.EmitGetHashCode(Definition.Indents12Spaces)).NewLine();
            return $@"
        public override int GetHashCode()
        {{
            int hash = BaseHash;
{body}
            return hash;
        }}";
        } 
        
        string EmitRegisterFormatter()
        {
            string body = Members.Where(x => x.Nullable && x.Kind == MemberKind.Unmanaged)
                .Select(x => x.EmitNullableStruct(Definition.Indents12Spaces)).NewLine();
            return $@"
        public static void RegisterFormatter()
        {{
            if (!{Definition.LFormatterProviderName}.IsRegistered<{TypeName}>())
            {{
                {Definition.LFormatterProviderName}.Register(new {TypeName}Formatter());
            }}
            if (!{Definition.LFormatterProviderName}.IsRegistered<{TypeName}[]>())
            {{
                {Definition.LFormatterProviderName}.Register(new {Definition.ArrayFormatterName}<{TypeName}>());
            }}
{body}
        }}";
        }

        void EmitFormatterImplement(StringBuilder writer, IGeneratorContext context)
        {
            var typeKind = IsValueType ? "struct" : "class"; 
            writer.AppendLine($@"
    public unsafe partial {typeKind} {TypeName}
    {{
        static {Symbol.Name}()
        {{
            RegisterFormatter();
        }}
        {EmitRegisterFormatter()}
        
        [Preserve]
        unsafe sealed class {Symbol.Name}Formatter : {Definition.LFormatterName}<{TypeName}>
        {{
            [Preserve]
            public override void Serialize({Definition.LSerializeContext}* context, {Definition.LStreamWriter}* writer, ref {TypeName} value)
            {{
                {TypeName}.Serialize(context, writer, ref value);
            }}

            [Preserve]
            public override void Deserialize({Definition.LSerializeContext}* context, {Definition.LStreamReader}* reader, ref {TypeName} value)
            {{
                {TypeName}.Deserialize(context, reader, ref value);
            }}
        }}
    }}");
        }
    }

    public partial class MemberMeta
    {
        public string EmitCheckValid(string indents, bool isStatic = false)
        {
            var name = isStatic ? $"value.{Name}" : Name;
            switch (Kind)
            {
                case MemberKind.BuiltIn:
                case MemberKind.Enum:
                case MemberKind.Unmanaged:
                case MemberKind.Struct:
                    return $"{indents}if ({name} != default) ";
                case MemberKind.String:
                    return $"{indents}if (!string.IsNullOrEmpty({name})) ";
                case MemberKind.Class:
                case MemberKind.Array:
                case MemberKind.List:
                case MemberKind.Dictionary:
                case MemberKind.Interface:
                    return $"{indents}if ({name} != null) ";
                case MemberKind.NonSerializable:
                default:
                    return "";
            } 
        }
        
        public string EmitValidFieldCount(string indents)
        {
            var prefix = EmitCheckValid(indents, true);
            return string.IsNullOrEmpty(prefix) ? "" : $"{prefix}total++;";
        }

        public string EmitEquatableCode(string indents, int total, bool isStatic = false)
        {
            var prefix = 0 == Order ? "" : indents;
            var suffix = total - 1 == Order ? "" : " && ";
            var selfName = isStatic ? "v1." : "";
            var otherName = isStatic ? "v2." : "value.";
            switch (Kind)
            {
                case MemberKind.Array:
                case MemberKind.List:
                    return $"{prefix}{selfName}{Name}.IsContentEqual({otherName}{Name}){suffix}";
                default:
                    return $"{prefix}{selfName}{Name} == {otherName}{Name}{suffix}";
            }
        }
        
        public string EmitGetHashCode(string indents)
        {
            var prefix = EmitCheckValid(indents);
            var body = "";
            switch (Kind)
            {
                case MemberKind.Array:
                case MemberKind.List:
                    body = $"{prefix}hash = hash * 31 + @{Name}.GetContentHash();";
                    break;
                default:
                    body = $"{prefix}hash = hash * 31 + @{Name}.GetHashCode();";
                    break;
            }
            return string.IsNullOrEmpty(prefix) ? "" : body;
        }
        
        public string EmitNullableStruct(string indents)
        {
            return @$"{indents}if (!{Definition.LFormatterProviderName}.IsRegistered<{MemberType.Name}?>())
            {{
                {Definition.LFormatterProviderName}.Register(new {Definition.NullableUnmanagedFormatter}<{MemberType.Name}>());
            }}";
        }
        
        public string EmitSerialize(string indents)
        {
            switch (Kind)
            {
                case MemberKind.BuiltIn:
                    return (Symbol.Kind == SymbolKind.Property)
                        ? $"{indents}writer->WriteNumber(context, value.@{Name});"
                        : $"{indents}writer->WriteNumber(context, ref value.@{Name});";
                case MemberKind.Enum:
                    return $"{indents}writer->WriteNumber(context, value.@{Name});";
                case MemberKind.String:
                    return $"{indents}writer->WriteString(context, ref value.@{Name});";
                case MemberKind.Unmanaged:
                    return $"{indents}writer->WriteUnmanagedStruct(context, ref value.@{Name});";
                case MemberKind.Class:
                    return $"{indents}writer->WriteManagedStruct(context, ref value.@{Name});";
                case MemberKind.Struct:
                    return $"{indents}writer->WriteStruct(context, ref value.@{Name});"; 
                case MemberKind.Array:
                    return $"{indents}writer->WriteArray<{TypeArguments[0].Name}>(context, ref value.@{Name});";
                case MemberKind.List:
                    return $"{indents}writer->WriteList<{TypeArguments[0].Name}>(context, ref value.@{Name});";
                case MemberKind.Dictionary:
                    return $"{indents}writer->WriteDictionary<{TypeArguments[0].Name}, {TypeArguments[1].Name}>(context, ref value.@{Name});";
                case MemberKind.Interface:
                    return $"{indents}writer->WriteReference(context, ref value.@{Name});";
                case MemberKind.NonSerializable:
                default:
                    return "";
            }  
        }
        
        public string EmitDeserialize(string indents)
        {
            switch (Kind)
            {
                case MemberKind.BuiltIn:
                    return (Symbol.Kind == SymbolKind.Property)
                        ? $"{indents}value.@{Name} = reader->ReadNumber<{MemberType.Name}>(context);"
                        : $"{indents}reader->ReadNumber(context, ref value.@{Name});";
                case MemberKind.Enum:
                    return $"{indents}value.@{Name} = reader->ReadNumber<{MemberType.Name}>(context);";
                case MemberKind.String:
                    return $"{indents}reader->ReadString(context, ref value.@{Name});";
                case MemberKind.Unmanaged:
                    return $"{indents}reader->ReadUnmanagedStruct(context, ref value.@{Name});"; 
                case MemberKind.Class:
                    return $"{indents}reader->ReadManagedStruct(context, ref value.@{Name});";
                case MemberKind.Struct:
                    return $"{indents}reader->ReadStruct(context, ref value.@{Name});";
                case MemberKind.Array:
                    return $"{indents}reader->ReadArray(context, ref value.@{Name});";
                case MemberKind.List:
                    return $"{indents}reader->ReadList(context, ref value.@{Name});";
                case MemberKind.Dictionary:
                    return $"{indents}reader->ReadDictionary(context, ref value.@{Name});";
                case MemberKind.Interface:
                    return $"{indents}reader->ReadReference(context, ref value.@{Name});";
                case MemberKind.NonSerializable:
                default:
                    return "";
            }  
        }
    }
}
